﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Elderos.Clustering;
using Elderos.Geometry;
using Elderos.Highlight.Search;
using Elderos.Ontology;
using Elderos.Utils.Extensions;

namespace Elderos.Highlight.Rules
{
    
    public abstract class RuleGeolocation : RankingRule
    {
        private double _degrees;

        public RuleGeolocation(IOntologyData ontology, double degreesCount) : base(ontology)
        {
            _degrees = degreesCount;
        }

        #region RankingRule members

        protected async override Task<Dictionary<TextEntity, IRuleResult>> InnerRank(HighlightResult highlightResult)
        {
            var results = new Dictionary<TextEntity, IRuleResult>();

            IEnumerable<EntityLocation> coordinates = await GetGeolocations(highlightResult);

            if (coordinates.IsNullOrEmpty()) return results;

            //Distance between point for clustering
            Func<EntityLocation, EntityLocation, double> objectsDistanse =
                (x, y) => Point.GetMinkovskyDistance(x.Location, y.Location);

            //Distance between point and cluster
            Func<EntityLocation, Cluster<EntityLocation>, double>
                clusterDistanse =
                    (x, y) => Point.GetMinkovskyDistance(x.Location, y.Centroid.Location);

            //Centroid function
            Func<IEnumerable<EntityLocation>, EntityLocation> getCenter =
                x =>
                new EntityLocation(-1,
                                   new Point(x.Average(y => y.Location.Coordinates[0]),
                                             x.Average(y => y.Location.Coordinates[1])));
            double delta = _degrees; //the cluster will be a circle with the radius of _degrees degrees
            var clusterizator = new ARTClusterizator<EntityLocation>(objectsDistanse, clusterDistanse, getCenter, delta);
            var clusters = clusterizator.Process(coordinates);

            //assigning weight has no sense
            if (clusters.Count() < 2) return results;

            foreach (var position in highlightResult.Positions)
            {
                foreach (var entity in position.EntitiesFlattened)
                {
                    var cluster = clusters.FirstOrDefault(x => x.Any(y => y.InnerElement.EntityID == entity.EntityID));

                    if (cluster == null) continue;

                    int clusterSize = cluster
                        .Select(x => x.InnerElement.EntityID)
                        .Except(position.EntitiesFlattened.Select(x => x.EntityID))
                        .Count();

                    if (clusterSize <= 0) continue;
                    
                    double coef = ComputeWeight(clusterSize);

                    results.Add(entity, new ContiniousResult(Name, coef));
                }
            }

            return results;
        }

        public override IRuleResult EmptyResult
        {
            get { return new ContiniousResult(Name, 0);}
        }

        private static double ComputeWeight(int clusterSize)
        {
            //return MathExtended.Sigmoid(clusterSize);
            return clusterSize;
        }

        private async Task<List<EntityLocation>> GetGeolocations(HighlightResult result)
        {
            var coordinates = new List<EntityLocation>();
            var entityIDs = new HashSet<int>(result.Entities.Select(x => x.EntityID));
            
            int geolocationAttID = Ontology.Attributes.First(x => x.Name == "/location/location/geolocation").ID;
            var attRefs = await Ontology.GetAttRefsAsync(new[] {geolocationAttID}, entityIDs);

            //extracting geolocation entities being attrefs of locations from ontology
            var geolocations = attRefs
                .Select(x => new {x.EntityID, x.ReferenceID})
                .GroupBy(x => x.ReferenceID)
                .ToDictionary(x => x.Key, x => x.Select(y => y.EntityID).ToList());

            int latitudeAttID = Ontology.Attributes.First(x => x.Name == "/location/geocode/latitude").ID;
            int longitudeAttID = Ontology.Attributes.First(x => x.Name == "/location/geocode/longitude").ID;
            List<int> geolocationEntitiesIDs = geolocations.Select(x => x.Key).ToList();

            //computing geolocation point for each known entity
            var latitudes = await Ontology.GetAttValuesAsync(new[] { latitudeAttID }, geolocationEntitiesIDs);
            var longitudes = await Ontology.GetAttValuesAsync(new[] { longitudeAttID }, geolocationEntitiesIDs);

            var geolocationCoordinates = latitudes
                .Join(longitudes,
                      x => x.EntityID,
                      x => x.EntityID,
                      (latitude, longitude) =>
                      new {
                              latitude.EntityID,
                              Latitude = latitude.Value,
                              Longitude = longitude.Value
                          });


            foreach (var coordinate in geolocationCoordinates)
            {
                foreach (int entityid in geolocations[coordinate.EntityID])
                {
                    double latitude = double.Parse(coordinate.Latitude, CultureInfo.InvariantCulture);
                    double longitude = double.Parse(coordinate.Longitude, CultureInfo.InvariantCulture);

                    var location = new Point(latitude, longitude);

                    coordinates.Add(new EntityLocation(entityid, location));
                }
            }

            return coordinates;
        }

        #endregion

        private class EntityLocation
        {
            public int EntityID { get; private set; }
            public Point Location { get; private set; }

            public EntityLocation(int entityID, Point location)
            {
                EntityID = entityID;
                Location = location;
            }
        }

    }
}
